package net.spatula.tally_ho.utils;

/**
 * Copyright (c) 2002-2006 Nick Johnson
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */

/**
 * FreeBSD-compatible md5-style password crypt,
 * based on crypt-md5.c by Poul-Henning Kamp, which was distributed
 * with the following notice:
 *
 * ----------------------------------------------------------------------------
 * "THE BEER-WARE LICENSE" (Revision 42):
 * <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
 * can do whatever you want with this stuff. If we meet some day, and you think
 * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
 * ----------------------------------------------------------------------------
 *
 * @author Nick Johnson <freebsd [at] spatula.net>
 * @version 1.1
 */

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * Class containing static methods for encrypting passwords in FreeBSD md5
 * style.
 * 
 */

public class FreeBSDCrypt {

	private FreeBSDCrypt() {
	}

	private static final String magic = "$1$";

	private static final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

	private static String cryptTo64(int value, int length) {
		StringBuffer output = new StringBuffer();

		while (--length >= 0) {
			output.append(itoa64.substring(value & 0x3f, (value & 0x3f) + 1));
			value >>= 6;
		}

		return (output.toString());
	}

	/**
	 * Encrypts a password using FreeBSD-style md5-based encryption
	 * 
	 * @param password
	 *            The cleartext password to be encrypted
	 * @param salt
	 *            The salt used to add some entropy to the encryption
	 * @throws java.security.NoSuchAlgorithmException
	 *             if java.security does not support MD5
	 * @return The encrypted password, or an empty string on error
	 */

	public static String crypt(String password, String salt) {

		/*
		 * First get the salt into a proper format. It can be no more than 8
		 * characters, and if it starts with the magic string, it should be
		 * skipped.
		 */

		if (salt.startsWith(magic)) {
			salt = salt.substring(magic.length());
		}

		int saltEnd = salt.indexOf('$');
		if (saltEnd != -1) {
			salt = salt.substring(0, saltEnd);
		}

		if (salt.length() > 8) {
			salt = salt.substring(0, 8);
		}

		/* now we have a properly formatted salt */

		MessageDigest md5_1, md5_2;

		try {
			md5_1 = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
			return null;
		}

		/* First we update one MD5 with the password, magic string, and salt */
		md5_1.update(password.getBytes());
		md5_1.update(magic.getBytes());
		md5_1.update(salt.getBytes());

		try {
			md5_2 = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
			return null;
		}

		/* Now start a second MD5 with the password, salt, and password again */
		md5_2.update(password.getBytes());
		md5_2.update(salt.getBytes());
		md5_2.update(password.getBytes());

		byte[] md5_2_digest = md5_2.digest();

		int md5Size = md5_2_digest.length; // XXX
		int pwLength = password.length();

		/*
		 * Update the first MD5 a few times starting at the first character of
		 * the second MD5 digest using the smaller of the MD5 length or password
		 * length as the number of bytes to use in the update.
		 */
		for (int i = pwLength; i > 0; i -= md5Size) {
			md5_1.update(md5_2_digest, 0, i > md5Size ? md5Size : i);
		}

		/*
		 * the FreeBSD code does a memset to 0 on "final" (md5_2_digest) here
		 * which may be a bug, since it references "final" again if the
		 * conditional below is true, meaning it always is equal to 0
		 */

		md5_2.reset();

		/*
		 * Again, update the first MD5 a few times, this time using either 0
		 * (see above) or the first byte of the password, depending on the
		 * lowest order bit's value
		 */
		byte[] pwBytes = password.getBytes();
		for (int i = pwLength; i > 0; i >>= 1) {
			if ((i & 1) == 1) {
				md5_1.update((byte) 0);
			} else {
				md5_1.update(pwBytes[0]);
			}
		}

		/*
		 * Set up the output string. It'll look something like $1$salt$ to begin
		 * with
		 */
		StringBuffer output = new StringBuffer(magic);
		output.append(salt);
		output.append("$");

		byte[] md5_1_digest = md5_1.digest();

		/*
		 * According to the original source, this bit of madness is introduced
		 * to slow things down. It also further mutates the result.
		 */
		byte[] saltBytes = salt.getBytes();
		for (int i = 0; i < 1000; i++) {
			md5_2.reset();
			if ((i & 1) == 1) {
				md5_2.update(pwBytes);
			} else {
				md5_2.update(md5_1_digest);
			}
			if (i % 3 != 0) {
				md5_2.update(saltBytes);
			}
			if (i % 7 != 0) {
				md5_2.update(pwBytes);
			}
			if ((i & 1) != 0) {
				md5_2.update(md5_1_digest);
			} else {
				md5_2.update(pwBytes);
			}
			md5_1_digest = md5_2.digest();
		}

		/* Reorder the bytes in the digest and convert them to base64 */
		int value;
		value = ((md5_1_digest[0] & 0xff) << 16)
				| ((md5_1_digest[6] & 0xff) << 8) | (md5_1_digest[12] & 0xff);
		output.append(cryptTo64(value, 4));
		value = ((md5_1_digest[1] & 0xff) << 16)
				| ((md5_1_digest[7] & 0xff) << 8) | (md5_1_digest[13] & 0xff);
		output.append(cryptTo64(value, 4));
		value = ((md5_1_digest[2] & 0xff) << 16)
				| ((md5_1_digest[8] & 0xff) << 8) | (md5_1_digest[14] & 0xff);
		output.append(cryptTo64(value, 4));
		value = ((md5_1_digest[3] & 0xff) << 16)
				| ((md5_1_digest[9] & 0xff) << 8) | (md5_1_digest[15] & 0xff);
		output.append(cryptTo64(value, 4));
		value = ((md5_1_digest[4] & 0xff) << 16)
				| ((md5_1_digest[10] & 0xff) << 8) | (md5_1_digest[5] & 0xff);
		output.append(cryptTo64(value, 4));
		value = md5_1_digest[11] & 0xff;
		output.append(cryptTo64(value, 2));

		/* Drop some hints to the GC */
		md5_1 = null;
		md5_2 = null;
		md5_1_digest = null;
		md5_2_digest = null;
		pwBytes = null;
		saltBytes = null;
		password = salt = null;

		return output.toString();
	}

}
